Jerry's Log

java folder structure

contents

사실 자바(JVM) 자체는 폴더 구조에 대해 아주 "멍청(dumb)"합니다. 자바는 "폴더 A"가 "폴더 B"의 형제라는 사실을 본능적으로 알지 못합니다.

구조를 감지하고 프로젝트들을 연결하는 마법은 자바 언어가 아니라 빌드 도구(Build Tools, Maven 또는 Gradle) 를 통해 일어납니다.

다음은 자바의 멀티 모듈 프로젝트가 작동하는 방식에 대한 상세 분석입니다.


1. 고수준 관점: "리액터 (The Reactor)"

자동차를 만든다고 상상해 봅시다. 엔진 부서, 바퀴 부서, 차체 부서가 따로 있습니다.

이 논리를 빌드 리액터(Build Reactor) 라고 부릅니다.


2. 물리적 구조 (디스크상의 모습)

일반적인 멀티 모듈 설정(기업 표준인 Maven을 예로 듭니다)에서 폴더 구조는 다음과 같습니다.

my-backend-system (루트 프로젝트)
├── pom.xml (부모 POM)
│
├── common-utils (하위 모듈 1: 공통 유틸리티)
│   ├── src/main/java...
│   └── pom.xml
│
├── user-service (하위 모듈 2: 사용자 서비스)
│   ├── src/main/java...
│   └── pom.xml
│
└── payment-service (하위 모듈 3: 결제 서비스)
    ├── src/main/java...
    └── pom.xml

3. 1단계: 감지 (부모 POM)

어떻게 자바/Maven은 common-utils 폴더가 my-backend-system의 일부라는 것을 알까요?

부모 pom.xml이 지도가 됩니다. <modules> 태그를 사용하여 어떤 폴더를 들여다봐야 하는지 명시적으로 알려줍니다.

파일: my-backend-system/pom.xml


    com.example
    my-backend-system
    1.0.0
    pom 
        common-utils
        user-service
        payment-service
    

4. 2단계: 연결 (의존성 관리)

그렇다면 user-service는 어떻게 common-utils 안에 있는 코드를 가져다 쓸까요?

단순히 import com.example.utils.StringHelper라고 쓴다고 되는 게 아닙니다. 의존성 연결(Dependency Link) 을 확립해야 합니다.

파일: user-service/pom.xml


    
        com.example
        my-backend-system
        1.0.0
    
    
    user-service

    
        
            com.example
            common-utils
            1.0.0
        
    

기술적으로 일어나는 일:

  1. 1단계 (Common-Utils): Maven이 common-utils를 컴파일합니다. .java 파일들을 .class로 바꾸고 common-utils-1.0.0.jar라는 압축 파일로 만듭니다.
  2. 2단계 (Install): Maven은 이 JAR 파일을 당신 컴퓨터의 로컬 저장소(Local Repository)(보통 ~/.m2/repository에 숨겨진 폴더)에 복사해 둡니다.
  3. 3단계 (User-Service): user-service가 컴파일될 때, Maven은 로컬 저장소를 뒤져서 common-utils JAR를 찾아내고, 이를 user-service클래스패스(Classpath) 에 추가합니다.

5. 3단계: 빌드 리액터 (그래프 해결)

이 부분이 가장 정교한 부분입니다.

루트 폴더에서 mvn compile을 입력하면, Maven은 단순히 알파벳 순서(Common, Payment, User)대로 빌드하지 않습니다. 의존성 그래프(Dependency Graph) 를 그립니다.

  1. user-servicecommon-utils가 필요하다는 것을 봅니다.
  2. payment-serviceuser-service가 필요하다는 것을 봅니다.
  3. 결과: 위상 정렬(Topological Sort) 을 계산하여 순서를 정합니다.

실행 순서:

common-utils $\rightarrow$ user-service $\rightarrow$ payment-service.

만약 순환 의존성(Circular Dependency)(A가 B를 필요로 하는데, B가 다시 A를 필요로 함)이 발생하면, 리액터는 이를 감지하고 즉시 에러를 띄우며 빌드를 중단합니다.


6. 내부 동작: 클래스패스 (런타임)

코드가 컴파일되고 실행되는 시점(Runtime)이 되면, "프로젝트"나 "모듈"이라는 개념은 사라집니다. JVM은 오직 클래스패스(Classpath) 만 이해합니다.

애플리케이션을 실행할 때, 실제 명령어는 다음과 같은 모습이 됩니다 (단순화함):

java -cp common-utils.jar:user-service.jar:spring-boot.jar com.example.user.Main

JVM은 여러분의 아름다운 계층형 폴더 구조를 납작하고 긴 JAR 파일 목록으로 만들어버립니다. user-service의 코드에서 import com.example.common.Utils를 호출하면, JVM은 목록에 있는 모든 JAR를 뒤져서 해당 클래스를 찾습니다.

7. 현대적 자바: JPMS (Java Platform Module System)

Java 9부터는 빌드 도구가 아니라 언어 자체에서 구조를 강제하는 방법이 도입되었습니다. 바로 module-info.java입니다.

파일: common-utils/src/main/java/module-info.java

module com.example.common {
    exports com.example.common.utils; // 이 패키지만 외부 노출
    // 다른 내부 패키지들은 효과적으로 "숨겨짐"
}

파일: user-service/src/main/java/module-info.java

module com.example.user {
    requires com.example.common; // 명시적으로 이 모듈이 필요하다고 선언
}

백엔드 개발자를 위한 요약

  1. 감지 (Detection): 루트 pom.xml<modules> 태그가 담당합니다.
  2. 연결 (Connection): 하위 프로젝트의 <dependency> 선언이 담당합니다.
  3. 순서 정렬 (Ordering): 리액터(Reactor) 가 의존성을 분석하여 올바른 빌드 순서를 계산합니다.
  4. 런타임 (Runtime): JVM은 모든 구조를 클래스패스(JAR 목록) 로 납작하게 만들어 실행합니다.

references